package de.hsb.ai.agent;

import game.Move;
import jade.content.Concept;
import jade.content.ContentElement;
import jade.content.onto.basic.Action;
import jade.content.onto.basic.Result;
import jade.core.AID;
import jade.core.Agent;
import jade.domain.FIPAAgentManagement.FailureException;
import jade.domain.FIPAAgentManagement.NotUnderstoodException;
import jade.domain.FIPAAgentManagement.RefuseException;
import jade.domain.introspection.EventRecord;
import jade.domain.introspection.Occurred;
import jade.lang.acl.ACLMessage;
import jade.lang.acl.MessageTemplate;
import jade.proto.SimpleAchieveREResponder;

import java.util.ArrayList;
import java.util.List;

import de.hsb.ai.domain.GameInfo;
import de.hsb.ai.domain.GameOver;
import de.hsb.ai.domain.GameStarted;
import de.hsb.ai.domain.MoveEvent;
import de.hsb.ai.domain.ScotlandJadeVocabulary;
import de.hsb.ai.domain.action.GetMove;
import de.hsb.ai.util.ScotlandJadeHelper;
import de.hsb.ai.util.map.ScotlandJadeMap;
import de.hsb.ai.util.map.ScotlandJadeMapHelper;
import de.hsb.ai.util.map.ScotlandJadeMapHelper.ScotlandJadeVertex;

/**
 * AbstractPlayer provides common player logic.
 * All devil players should inherit from this class.
 */
@SuppressWarnings("serial")
public abstract class AbstractPlayer extends AbstractAgent {
	
	/**
	 * The abstract service-type.
	 */
	public static final String SERVICE_TYPE = "sy.player";
	
	public static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(SERVICE_TYPE);
	
	protected static final String PREFIX_SEPARATOR = "-";
	
	/**
	 * Representation of the current state of the board.
	 */
	protected final ScotlandJadeMap map = ScotlandJadeMapHelper.createMap();
	
	/**
	 * Positions by player index.
	 */
	protected final List<ScotlandJadeVertex> positions = new ArrayList<ScotlandJadeVertex>(ScotlandJadeVocabulary.MAX_PLAYERS);
	
	protected final ScotlandJadeVertex.Type vertexType;
	
	private int playerIndex = -1;
	
	/**
	 * The current round of the game.
	 */
	
	private int round = 0; 
	
	/**
	 * Information about the current game.
	 */
	private GameInfo gameInfo;
	
	/**
	 * Creates a new {@link AbstractPlayer}.
	 * @param serviceNamePrefix prefix to the service name
	 * @see #SERVICE_TYPE
	 */
	public AbstractPlayer(ScotlandJadeVertex.Type type, String serviceNamePrefix) {
		super(serviceNamePrefix + PREFIX_SEPARATOR + SERVICE_TYPE, SERVICE_TYPE);
		vertexType = type;
	}
	
	
	@Override
	protected void setup() {
		super.setup();
		
		addBehaviour(new Responder(this, 
				ScotlandJadeHelper.createMessageTemplate(
						ACLMessage.REQUEST, ACLMessage.INFORM, ACLMessage.REFUSE)));
	}
	
	protected ACLMessage handleAction(ACLMessage request, Concept action) {
		ACLMessage rep = request.createReply();
		rep.setPerformative(ACLMessage.NOT_UNDERSTOOD);
		
		if (action instanceof GetMove) {
			rep.setPerformative(ACLMessage.INFORM);
			List<Move> options = ((GetMove)action).getOptions();
			Result res = new Result(action, getMove(options));
//			logger.debug("taking " + res.getValue());
			try {
				getContentManager().fillContent(rep, res);
			} catch (Exception e) {
				logger.error("creating action response", e);
			}
		}
		return rep;
	}
	
	protected ACLMessage handleEvent(ACLMessage request, EventRecord event) {
//		logger.debug("handle event");
		ACLMessage rep = request.createReply();
		
		if (event.getWhat() instanceof MoveEvent) {
			handleMove((MoveEvent) event.getWhat());
		} else if (event.getWhat() instanceof GameOver) {
			handleGameOver((GameOver) event.getWhat());
		} else if (event.getWhat() instanceof GameStarted) {
			handleGameStarted((GameStarted) event.getWhat());
		}
		
		return rep;
	}
	
	/**
	 * Retraces the move as part of the specified event in the {@link #map}.
	 * This may be overridden for special event handling.
	 * @param event the move event
	 */
	protected void handleMove(MoveEvent m) {
		boolean ownMove = m.getPlayer().equals(getAID());
		boolean fugMove = m.getPlayerIndex() == 0;
		
		round = m.getRound();		
		
		// Moves starting from 0 aren't real moves
		// they indicate a player being setup or
		// the appearance of the fugitive.
		if (m.getFrom() == 0) {
			// Set the 0-vertex to our vertex-type
			// so the move mechanism works predictably.
			map.get(0).setType(ownMove 
					? vertexType : fugMove 
							? ScotlandJadeVertex.Type.FUGITIVE 
									: ScotlandJadeVertex.Type.DETECTIVE);
		}
		
		// Perform move on map/graph
		positions.set(m.getPlayerIndex(), map.move(m.getFrom(), m.getMove()));
	}
	
	/**
	 * Called when a new game has been started.
	 * Stores the {@link GameInfo} in {@link #gameInfo}.
	 * Override for custom event handling.
	 * @param g the event
	 */
	protected void handleGameStarted(GameStarted g) {
		round	 = 0;
		gameInfo = g.getInfo();

		map.reset();
		positions.clear();
		playerIndex = - 1;
		
		for (AID player : gameInfo.getPlayers()) {
			positions.add(map.get(0));
			if (player.equals(getAID())) {
				playerIndex = positions.size() - 1;
			}
		}
		
	}
	
	/**
	 * Called when a game has ended.
	 * Override for custom event handling.
	 * @param g the event
	 */
	protected void handleGameOver(GameOver g) {
		if (g.getWinner().equals(getAID())) {
			logger.info("I won!");
		}
	}
	
	/**
	 * Returns the current round.
	 * @return the current round
	 */
	public int getRound() {
		return round;
	}
	
	/**
	 * Returns information about the current game.
	 * @return a {@link GameInfo} object holding a list of player-{@link AID AIDs}
	 * @throws IllegalStateException if the {@link GameInfo} object hasn't been initialized yet,
	 * that is when this method is called before {@link #handleGameStarted(GameStarted)} has been called
	 */
	protected GameInfo getGameInfo() {
		if (gameInfo == null)
			throw new IllegalStateException(
					"game info has not been initalized yet, did you try to call this outside a game?");
		
		return gameInfo;
	}
	
	protected int getPlayerIndex() {
		return playerIndex;
	}
	
	/**
	 * Called when it's the this agents turn to make a move.
	 * @param options possible moves
	 * @return the move picked
	 */
	protected abstract Move getMove(List<Move> options);
	
	
	/**
	 * Player response-behavior.
	 */
	private class Responder extends SimpleAchieveREResponder {

		public Responder(Agent a, MessageTemplate mt) {
			super(a, mt);
		}
		
		@Override
		protected ACLMessage prepareResponse(ACLMessage request) throws NotUnderstoodException, RefuseException {
			try {
				ContentElement ce = getContentManager().extractContent(request);
				if (ce instanceof Action) {
					// handle action
					return handleAction(request, ((Action)ce).getAction());
				} else if (ce instanceof Occurred) {
					// handle event
					return handleEvent(request, ((Occurred)ce).getWhat());
				} else if (ce instanceof Result && request.getPerformative() == ACLMessage.REFUSE) {
					// Retry after refuse (not very elegant)
					// TODO implement some kind of retry-counter
					return handleAction(request, ((Result)ce).getAction());
				} else {
					logger.warn("unkown action/event");
					throw new NotUnderstoodException("unkonw action/event");
				}
			} catch (Exception e) {
				logger.error("unexpected request", e);
				throw new NotUnderstoodException("unexpected request");
			}
		}
		
		@Override
		protected ACLMessage prepareResultNotification(ACLMessage request,
				ACLMessage response) throws FailureException {
			return null;
		}
	}
	
}
